from datetime import datetime

from sqlalchemy import Table, Integer, Unicode, Column, DateTime, ForeignKey, select, bindparam, and_, desc
from sqlalchemy.orm import mapper, object_mapper, object_session, ColumnProperty, relation, synonym, column_property, backref

from brainfreeze import OneToOneMapperProperty

from tortuga.database import metadata
from tortuga.models.util import Base, effective_between
from tortuga.models.content import Content, content_table
from tortuga.models.workflow import WorkflowEvent, WORKFLOW_STATUS, workflow_event_table

__all__ = ['Revision', 'revision_table', 'CONTENT_STATUS']

CONTENT_STATUS = dict(
    CREATED=0,
    UPDATED=1,
    RESTORED=2,
    DELETED=3
)

# one record per change made to anything.
# Note that content_id will be null when something is revisioned but isn't really a 
# content type on its own -- for example many-to-many secondaryjoin objects will utilize this.
revision_table = Table('revision', metadata,
    Column('revision_id', Integer, primary_key=True),
    Column('content_id', None, ForeignKey('content.content_id'), nullable=True), 
    Column('content_status', Integer, default=CONTENT_STATUS['CREATED'], nullable=False),
    Column('locale', Unicode(4), default=u'en', nullable=True), # TODO: move default to config
    Column('release_date', DateTime, nullable=True),
    Column('expiration_date', DateTime, nullable=True)
)

class Revision(Base):
    """Represents a generic revisioned content item"""
    def __init__(self, **kwargs):
        """Instantiate a new Revision"""
        # Locate the mapper for the current class (Revision or a subclass)
        current_mapper = object_mapper(self)
        
        # Create the initial Workflow object using status=DRAFT or a status supplied in kwargs
        workflow_property = current_mapper.get_property('_workflow_events')
        workflow_class = workflow_property.mapper.class_
        self._workflow_events.append(
            workflow_class(status=kwargs.get('workflow_status', WORKFLOW_STATUS['DRAFT']))
        )
        
        # Create a Content object or use the content record supplied in kwargs
        content_id = kwargs.get('content_id', None)
        content_obj = kwargs.get('_content', None)
        if content_id is None:
            content_property = current_mapper.get_property('_content')
            content_class = content_property.mapper.class_
            content_type = current_mapper.polymorphic_identity
            if content_obj is None:
                kwargs['_content'] = content_class(content_type=content_type)
            elif content_obj.content_type is None:
                content.content_type = content_type

        # Let SQLAlchemy take care of the rest...
        super(Revision, self).__init__(**kwargs)
        
    def submit(self, comment=None):
        """Submit this item for approval"""
        # Locate the mapper for the current instance (Revision or subclass)
        current_mapper = object_mapper(self)
        # Locate the class for the workflow objects
        workflow_property = current_mapper.get_property('_workflow_events')
        workflow_class = workflow_property.mapper.class_
        self._workflow_events.append(
            workflow_class(status=WORKFLOW_STATUS['PENDING'], comment=comment)
        )

    def approve(self, comment=None):
        """Approve this item for publishing"""
        # Locate the mapper for the current instance (Revision or subclass)
        current_mapper = object_mapper(self)
        # Locate the class for the workflow objects
        workflow_property = current_mapper.get_property('_workflow_events')
        workflow_class = workflow_property.mapper.class_
        self._workflow_events.append(
            workflow_class(status=WORKFLOW_STATUS['APPROVED'], comment=comment)
        )

    def decline(self, comment=None):
        """Decline to approve this item and set it back to DRAFT mode"""
        # Locate the mapper for the current instance (Revision or subclass)
        current_mapper = object_mapper(self)
        # Locate the class for the workflow objects
        workflow_property = current_mapper.get_property('_workflow_events')
        workflow_class = workflow_property.mapper.class_
        self._workflow_events.append(
            workflow_class(status=WORKFLOW_STATUS['DRAFT'], comment=comment)
        )

    def __workflow_status(self):
        """Return the status of the most recently added workflow event.
        
        Note that the workflow event may not have been flushed yet.  This property
        overrides the 'workflow_status' mapper property, which only knows about
        workflow events that have been persisted to the database.
        
        """
        if self._workflow_events:
            return self._workflow_events[-1].status
        else:
            return None
    workflow_status = property(__workflow_status)

    def __is_effective(self):
        """Return true if datetime.now() is between the release and expiration dates.
        
        Note that this is done in realtime.  This property overrides the 'is_effective'
        mapper property, which only knows if the instance was effective at the time that
        the object came out of the database.
        
        """
        now = datetime.now()
        return (
            ((self.release_date is None) or (self.release_date <= now))
            and
            ((self.expiration_date is None) or (now <= self.expiration_date))
        )
    is_effective = property(__is_effective)

    def __get_history(self):
        """Return a list of all prior revisions of this content object"""
        class_ = self.__class__
        return object_session(self).query(class_).filter(
            and_(
                class_.content_id==self.content_id, 
                class_.revision_id<self.revision_id
            )
        ).all()
    history = property(__get_history)

    def new_revision(self, _readonly_properties=None, 
        _revisioned_onetoone_relations=None, **kwargs):
        """Return a new revision of this instance.
        
        _readonly_properties
            A list of key values for mapper properties whose values should NOT be copied
            from the old instance to the new instance.  Typically this is handled
            by a classmethod override (ex: Page.new_revision).
            
        _revisioned_onetoone_relations
            A list of key values for OneToOnePropertyLoader relations that should be
            revisioned.  If a kwarg is specified for any proxied property of these
            relations, a new relation instance will be created.  For example,
            if p1 is a Page, and p1.title is a OneToOne property that proxies to
            p1._page_localized.title, then specifying a title as a kwarg will result
            in a new version of _page_localized (assuming 'page_localized' 
            is in _revisioned_onetoone_relations)
        
        **kwargs
            Properties specified in kwargs will be used on the new instance,
            so for example, r2 = r1.new_revision(title='foobar') 
            will return a new revision r2 with r2.title set to 'foobar'.
        
        """
        # Determine which onetoone relations should be revisioned
        self._revisioned_onetoone_relations = []
        if _revisioned_onetoone_relations:
            self._revisioned_onetoone_relations.extend(_revisioned_onetoone_relations)

        # Determine which properties *not* to copy over to new instance.
        self._readonly_properties = [
            'is_approved',
            '_is_effective',
            'is_effective',
            '_workflow_events',
            'is_latest_version',
            'workflow_status',
            'is_latest_approved_version',
            '_workflow_status',
            'revision_id',
            'content_type'
        ]
        if _readonly_properties:
            self._readonly_properties.extend(_readonly_properties)

        # Locate the mapper for the current instance (Revision or a subclass)
        current_mapper = object_mapper(self)

        # Keep track of properties to remove from properties_dict using this marker
        do_not_copy = object()

        # Start filling a dictionary with the properties for the new revision
        properties_dict = dict()
        for key, value in kwargs.iteritems():
            properties_dict.setdefault(key, value)
            # For any OneToOneMapperProperty properties specified in kwargs,
            # that use a one-to-one relation specified in _revisioned_onetoone_relations,
            # set things up so a new instance of the related object will be created.
            # i.e. Page.new_revision(title="Home") should create a new Page.Localized instance
            prop = current_mapper.get_property(key, raiseerr=False)
            if isinstance(prop, OneToOneMapperProperty):    
                # Determine the relation this property is proxying for
                relation_class = prop.descriptor.target_class # ex: PageLocalized
                relation_name = prop.descriptor.target_collection # ex: '_page_localized'
                relation = current_mapper.get_property(relation_name)
                # If the relation instance should be revisioned, set things up so
                # a new relation instance will be created.
                if relation_name in self._revisioned_onetoone_relations:
                    properties_dict.setdefault(relation_name, do_not_copy)
                    # New instances are created for objects with null IDs, so have at it!
                    for p in current_mapper.iterate_properties:
                        if isinstance(p, ColumnProperty):
                            for c in p.columns:
                                if c in relation.local_side:
                                    # All this, just to unset a foreign key column
                                    properties_dict.setdefault(p.key, do_not_copy)
            
        # Copy instance values for any desired properties that weren't found in kwargs
        for p in current_mapper.iterate_properties:
            if p.key not in kwargs and p.key not in self._readonly_properties:
                properties_dict.setdefault(p.key, getattr(self, p.key))
                
        # Remove properties that aren't supposed to be copied to the new instance:
        for k in properties_dict.keys():
            if properties_dict.get(k, None) is do_not_copy:
                del properties_dict[k]

        # Unless otherwise specified, assume this new revision is an UPDATE,
        # as opposed to a RESTORE or a DELETE.
        properties_dict.setdefault('content_status', CONTENT_STATUS['UPDATED'])

        # Finally, create and return the new revision
        return current_mapper.class_(**properties_dict)
        
revision_content_join = select(
    [revision_table, content_table.c.content_type],
    from_obj=[revision_table.join(content_table)]
).alias('revision_content_join')

revision_content_alias = revision_content_join.alias('revision_content_alias')
workflow_alias = workflow_event_table.alias('workflow_alias')

mapper(Revision, revision_content_join, 
    polymorphic_on=revision_content_join.c.content_type, polymorphic_identity=u'revision',
    properties={
        '_content': relation(Content, 
            backref=backref('revisions', cascade="all, delete, delete-orphan")
        ),
        '_workflow_events': relation(WorkflowEvent, cascade="all, delete, delete-orphan"),
        '_workflow_status': column_property(
            select(
                [workflow_event_table.c.status],
                workflow_event_table.c.revision_id==revision_content_join.c.revision_id,
                order_by=desc(workflow_event_table.c.workflow_id),
                limit=1
            ).label('latest_workflow_status'),
            # deferred=True
        ),
        'workflow_status': synonym('_workflow_status', map_column=False),
        'is_latest_version': column_property(
            revision_content_join.c.revision_id==select( # select id of latest revision in this locale
                [revision_table.c.revision_id],
                and_(
                    revision_table.c.content_id==revision_content_join.c.content_id,
                    revision_table.c.locale==revision_content_join.c.locale
                ),
                order_by=desc(revision_table.c.revision_id),
                limit=1
            ).label('latest_revision_id')
        ),
        '_is_effective': column_property(
            effective_between(
                bindparam('fetch_time', datetime.now),
                revision_content_join.c.release_date, 
                revision_content_join.c.expiration_date
            ).label('is_effective') == True,
            # deferred=True
        ),
        'is_effective': synonym('_is_effective', map_column=False),
        'is_approved': column_property(
            select(
                [workflow_event_table.c.status],
                workflow_event_table.c.revision_id==revision_content_join.c.revision_id,
                order_by=desc(workflow_event_table.c.workflow_id),
                limit=1
            ).label('latest_workflow_status') == WORKFLOW_STATUS['APPROVED']
        ),
        'is_latest_approved_version': column_property(
            revision_content_join.c.revision_id==select( # select id of latest approved revision in this locale
                [revision_table.c.revision_id],
                and_(
                    revision_table.c.content_id==revision_content_join.c.content_id,
                    revision_table.c.locale==revision_content_join.c.locale,
                    select(
                        [workflow_event_table.c.status],
                        workflow_event_table.c.revision_id==revision_table.c.revision_id,
                        order_by=desc(workflow_event_table.c.workflow_id),
                        limit=1
                    ).label('latest_workflow_status') == WORKFLOW_STATUS['APPROVED'],
                ),
                order_by=desc(revision_table.c.revision_id),
                limit=1
            ).label('latest_approved_revision_id')
        ),
        'is_latest_approved_effective_version': column_property(
            revision_content_join.c.revision_id==select( # select id of latest approved effective revision in this locale
                [revision_table.c.revision_id],
                and_(
                    revision_table.c.content_id==revision_content_join.c.content_id,
                    revision_table.c.locale==revision_content_join.c.locale,
                    select(
                        [workflow_event_table.c.status],
                        workflow_event_table.c.revision_id==revision_table.c.revision_id,
                        order_by=desc(workflow_event_table.c.workflow_id),
                        limit=1
                    ).label('latest_workflow_status') == WORKFLOW_STATUS['APPROVED'],
                    effective_between(
                        bindparam('fetch_time', datetime.now),
                        revision_table.c.release_date,
                        revision_table.c.expiration_date
                    ).label('is_effective') == True,
                ),
                order_by=desc(revision_table.c.revision_id),
                limit=1
            ).label('latest_approved_effective_version_id')
        ),
        'is_live': column_property(
            and_(
                revision_content_join.c.content_status != CONTENT_STATUS['DELETED'], # item is not deleted
                revision_content_join.c.revision_id==select( # select id of latest approved effective revision in this locale
                    [revision_table.c.revision_id],
                    and_(
                        revision_table.c.content_id==revision_content_join.c.content_id,
                        revision_table.c.locale==revision_content_join.c.locale,
                        select(
                            [workflow_event_table.c.status],
                            workflow_event_table.c.revision_id==revision_table.c.revision_id,
                            order_by=desc(workflow_event_table.c.workflow_id),
                            limit=1
                        ).label('latest_workflow_status') == WORKFLOW_STATUS['APPROVED'],
                        effective_between(
                            bindparam('fetch_time', datetime.now),
                            revision_table.c.release_date,
                            revision_table.c.expiration_date
                        ).label('is_effective') == True,
                    ),
                    order_by=desc(revision_table.c.revision_id),
                    limit=1
                ).label('latest_approved_effective_version_id')
            ).label('is_live')
        )
    }
)